/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

/*
 * Created on 26/10/2004
 *
 */

package au.com.lastweekend.jim.swing.tree;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.IOException;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreePath;

import org.apache.log4j.Logger;

import au.com.lastweekend.jim.swing.PopupMenuMouseListener;
import au.com.lastweekend.jim.swing.SwingUtil;

/**
 * DnD tree controller
 * 
 * Popup menu to Edit/Add/Remove treepaths.
 * 
 * Drag and drop to move items around the tree
 * 
 * This class simply manipulates MutableTreeModel and MutableTreeNodes, if you want to do something useful with this node data
 * (like update a database), override it and extend the addNode, removeNode, moveNode and editNode methods, calling super() as
 * appropriate if you want the actions to proceed.
 * 
 * TODO bind "Ins" to AddAction and "Del" to RemoveAction
 * 
 * @author grant@lastweekend.com.au
 * @version $Id: EditableKeywordTree.java,v 1.3 2006/02/04 00:31:36 ggardner Exp $
 */
@SuppressWarnings("serial")
public class MutableTreeController
        implements
            CellEditorListener,
            DragSourceListener,
            DragGestureListener,
            DropTargetListener,
            TreeSelectionListener,
            Transferable {

    private static final Logger LOG = Logger.getLogger(MutableTreeController.class);
    private static final int AUTOSCROLL_INSET_X = 20;
    private static final int AUTOSCROLL_INSET_Y = 20;

    private final class RemoveAction extends AbstractAction {

        RemoveAction() {

            super("Remove");
            setEnabled(false);
        }

        public void actionPerformed(ActionEvent e) {

            if (this.isEnabled()) {
                if (tree.isEditable()) {
                    tree.cancelEditing();
                    TreePath path = tree.getSelectionPath();

                    if (path != null) {
                        MutableTreeModel model = (MutableTreeModel) tree.getModel();
                        MutableTreeNode node = (MutableTreeNode) path.getLastPathComponent();

                        removeNode(model, node);
                    }

                }
            }
        }
    }

    private final class EditAction extends AbstractAction {

        EditAction() {

            super("Edit");
            setEnabled(false);
        }

        public void actionPerformed(ActionEvent event) {

            if (this.isEnabled()) {
                final TreePath path = tree.getSelectionPath();

                startEditingAtNode(path);
            }

        }
    }

    private final class AddAction extends AbstractAction {

        AddAction() {

            super("Add");
            setEnabled(true);
        }

        public void actionPerformed(ActionEvent event) {

            if (this.isEnabled()) {
                if (tree.isEditable()) {
                    TreePath path = tree.getSelectionPath();
                    tree.cancelEditing();

                    MutableTreeNode node = pathToNode(path);

                    if (node.getAllowsChildren()) {
                        MutableTreeModel model = (MutableTreeModel) tree.getModel();
                        addNode(model, node, createNode());
                    }
                }
            }

        }
    }

    private MutableTreeNode pathToNode(TreePath path) {

        MutableTreeNode node;
        if (path == null) {
            node = (MutableTreeNode) tree.getModel().getRoot();
        } else {
            node = (MutableTreeNode) path.getLastPathComponent();

        }
        return node;
    }
    private final Action editAction = new EditAction();
    private final Action addAction = new AddAction();
    private final Action removeAction = new RemoveAction();

    private TreePath dragPath;
    private TreePath dropPath;

    private JTree tree;

    private static final DataFlavor TREE_NODE_DATA_FLAVOUR = new DataFlavor(MutableTreeNode.class, MutableTreeNode.class
            .getCanonicalName());

    public MutableTreeController(JTree tree) {

        this.tree = tree;
        tree.setEditable(true);
        tree.getCellEditor().addCellEditorListener(this);
        tree.addTreeSelectionListener(this);
        tree.setSelectionPath(null);
        buildPopupMenu();
        setupDragAndDrop();
        bindKeyActions();
    }

    /**
     * 
     */
    private void bindKeyActions() {

        KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0);
        tree.getInputMap(JComponent.WHEN_FOCUSED).put(keyStroke, addAction.getValue(Action.NAME));
        SwingUtil.bindKeyAction(tree, addAction);

        keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);
        tree.getInputMap(JComponent.WHEN_FOCUSED).put(keyStroke, removeAction.getValue(Action.NAME));
        SwingUtil.bindKeyAction(tree, removeAction);

    }

    /**
     * Create a new node.
     * 
     * @return
     */
    protected MutableTreeNode createNode() {

        return new DefaultMutableTreeNode();
    }

    /**
     * Add node and start editing.
     * 
     * Overrides should note that the child has not yet been added to the tree.
     * 
     * @param model
     * @param parent
     * @param child
     */
    protected void addNode(MutableTreeModel model, MutableTreeNode parent, MutableTreeNode child) {

        model.insertNodeInto(child, parent, 0);

        startEditingAtNode(SwingUtil.getTreePath(child));
    }

    /**
     * Removes the node from the model.
     * 
     * Overrides should note that the node will likely be changed by this.
     * 
     * @param model
     * @param node
     */
    protected void removeNode(MutableTreeModel model, MutableTreeNode node) {

        model.removeNodeFromParent(node);
    }

    /**
     * Inserts the node into the model, override if you want to do something with the data in the node, noting that it will be
     * changed if you call this method via super()
     * 
     * @param model
     * @param fromNode
     * @param toNode
     */
    protected void moveNode(MutableTreeModel model, MutableTreeNode fromNode, MutableTreeNode toNode) {

        if (fromNode.getParent() != null) {
            // We previously had a parent, better remove it
            model.removeNodeFromParent(fromNode);
        }

        model.insertNodeInto(fromNode, toNode, 0);
    }

    /**
     * @param editingPath
     * @param newEditorValue
     */
    protected void nodeChanged(MutableTreeNode node, Object newEditorValue) {

        /*
         * this is already in response to the cell editor, the tree UI will call valueForPathChanged on there so we don't have to
         * do that here.
         */

    }

    private void buildPopupMenu() {

        final JPopupMenu popup = new JPopupMenu("Editing");
        popup.add(new JMenuItem(addAction));
        popup.add(new JMenuItem(editAction));
        popup.add(new JMenuItem(removeAction));

        MouseListener popupListener = new PopupMenuMouseListener(popup) {

            @Override
            protected void tryPopup(final MouseEvent e) {

                if (e.isPopupTrigger()) {

                    int x = e.getX();
                    int y = e.getY();
                    int closestRow = tree.getClosestRowForLocation(x, y);
                    // if we are below the top of the bounds and to the right and above the bottom + some offset then use the
                    // closest row otherwise we will allow adding to the root.
                    Rectangle rowBounds = tree.getRowBounds(closestRow);

                    if (y >= rowBounds.y && y <= rowBounds.y + rowBounds.height && x >= rowBounds.x) {
                        tree.setSelectionRow(closestRow);
                    } else {
                        tree.setSelectionRow(-1);
                    }
                    popup.show(e.getComponent(), e.getX(), e.getY());

                }

            }
        };
        tree.addMouseListener(popupListener);
    }

    /**
     * 
     */
    private void setupDragAndDrop() {

        DragSource dragSource = DragSource.getDefaultDragSource();
        dragSource.createDefaultDragGestureRecognizer(tree, DnDConstants.ACTION_COPY_OR_MOVE, this);
        new DropTarget(tree, DnDConstants.ACTION_COPY_OR_MOVE, this);
    }

    // Drag source listener methods, all empty
    public void dragEnter(DragSourceDragEvent dsde) {

        setDragSourceCursor(dsde);
    }

    public void dragOver(DragSourceDragEvent dsde) {

        setDragSourceCursor(dsde);
    }

    public void dropActionChanged(DragSourceDragEvent dsde) {

        setDragSourceCursor(dsde);

    }

    private void setDragSourceCursor(DragSourceDragEvent dsde) {

        int action = dsde.getDropAction();
        if (action == DnDConstants.ACTION_COPY) {
            dsde.getDragSourceContext().setCursor(DragSource.DefaultCopyDrop);
        } else {
            if (action == DnDConstants.ACTION_MOVE) {
                dsde.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
            } else {
                dsde.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
            }
        }
    }

    public void dragDropEnd(DragSourceDropEvent dsde) {

    }

    public void dragExit(DragSourceEvent dse) {

        dse.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
    }

    /*
     * @see java.awt.dnd.DragGestureListener#dragGestureRecognized(java.awt.dnd.DragGestureEvent)
     */
    public void dragGestureRecognized(DragGestureEvent dge) {

        // Find the path for the cursor position.
        Point p = dge.getDragOrigin();

        dragPath = tree.getPathForLocation(p.x, p.y);
        if (dragPath == null) {
            return;
        }

        LOG.debug("Starting drag of " + dragPath.getLastPathComponent());
        dge.startDrag(DragSource.DefaultMoveNoDrop, this, this);
    }

    /*
     * @see java.awt.dnd.DropTargetListener#dragEnter(java.awt.dnd.DropTargetDragEvent)
     */
    public void dragEnter(DropTargetDragEvent dtde) {

        dragOver(dtde);

    }

    /**
     * As we drag around the target, keep track of which point we are over <br>
     * If necessary repaint the display <br>
     * Handle vertical scrolling as we go.
     * 
     * @see java.awt.dnd.DropTargetListener#dragOver(java.awt.dnd.DropTargetDragEvent)
     */
    public void dragOver(DropTargetDragEvent dtde) {

        Point newDropPoint = dtde.getLocation();
        tree.scrollRectToVisible(new Rectangle(newDropPoint.x - AUTOSCROLL_INSET_X / 2, newDropPoint.y - AUTOSCROLL_INSET_Y / 2,
                AUTOSCROLL_INSET_X, AUTOSCROLL_INSET_Y));

        dropPath = tree.getPathForLocation(newDropPoint.x, newDropPoint.y);
        if (dropPath == null) {
            if (!tree.isRootVisible()) {
                // TODO, we really should use some top/left area to represent the root, but not right and bottom.
                dropPath = new TreePath(tree.getModel().getRoot());
            }
        }

        if (validDrop(dragPath, dropPath)) {
            dtde.acceptDrag(dtde.getDropAction());
        } else {
            dtde.rejectDrag();
        }

    }

    /**
     * @param draggingPath
     * @param droppingPath
     * @return
     */
    private boolean validDrop(TreePath draggingPath, TreePath droppingPath) {

        if (droppingPath == null || draggingPath == null) {
            return false;
        }

        // Don't allow a parent to be dropped on one of its children.
        if (draggingPath.isDescendant(droppingPath)) {
            return false;
        }

        // No point dropping an item on it's own parent.
        if (droppingPath.equals(draggingPath.getParentPath())) {
            return false;
        }

        MutableTreeNode node = (MutableTreeNode) droppingPath.getLastPathComponent();
        return node.getAllowsChildren();

        // TODO Prevent duplicate children.

    }

    public void dropActionChanged(DropTargetDragEvent dtde) {

    }

    public void drop(DropTargetDropEvent dtde) {

        if (dropPath == null) {
            return;
        }

        MutableTreeNode dropNode = (MutableTreeNode) dropPath.getLastPathComponent();
        MutableTreeNode dragNode = (MutableTreeNode) dragPath.getLastPathComponent();

        moveNode((MutableTreeModel) tree.getModel(), dragNode, dropNode);

    }

    /*
     * @see java.awt.dnd.DropTargetListener#dragExit(java.awt.dnd.DropTargetEvent)
     */
    public void dragExit(DropTargetEvent dte) {

        dropPath = null;

    }

    /*
     * @see java.awt.datatransfer.Transferable#getTransferDataFlavors()
     */
    public DataFlavor[] getTransferDataFlavors() {

        DataFlavor[] flavors = {TREE_NODE_DATA_FLAVOUR};
        return flavors;
    }

    /*
     * @see java.awt.datatransfer.Transferable#isDataFlavorSupported(java.awt.datatransfer.DataFlavor)
     */
    public boolean isDataFlavorSupported(DataFlavor flavor) {

        return flavor == TREE_NODE_DATA_FLAVOUR;
    }

    /*
     * @see java.awt.datatransfer.Transferable#getTransferData(java.awt.datatransfer.DataFlavor)
     */
    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {

        return dragPath.getLastPathComponent();
    }

    private void startEditingAtNode(final TreePath path) {

        SwingUtilities.invokeLater(new Runnable() {

            public void run() {

                tree.startEditingAtPath(path);

            }
        });
    }

    public Action getEditAction() {

        return editAction;
    }

    public Action getAddAction() {

        return addAction;
    }

    public Action getRemoveAction() {

        return removeAction;
    }

    public void editingCanceled(ChangeEvent e) {

        // do nothing.

    }

    public void editingStopped(ChangeEvent e) {

        TreeCellEditor editor = ((TreeCellEditor) e.getSource());
        Object newValue = editor.getCellEditorValue();
        // this listener is registered after the tree ui so getEditingPath() should be valid as
        // the tree processes the listeners in reverse order

        nodeChanged((MutableTreeNode) tree.getEditingPath().getLastPathComponent(), newValue);

    }

    public void valueChanged(TreeSelectionEvent e) {

        MutableTreeNode node = pathToNode(e.getNewLeadSelectionPath());
        addAction.setEnabled(shouldAddActionBeEnabled(node));
        editAction.setEnabled(shouldEditActionBeEnabled(node));
        removeAction.setEnabled(shouldRemoveActionBeEnabled(node));

    }

    /**
     * @param node
     * @return
     */
    protected boolean shouldRemoveActionBeEnabled(MutableTreeNode node) {

        return node != null;
    }

    /**
     * @param node
     * @return
     */
    protected boolean shouldEditActionBeEnabled(MutableTreeNode node) {

        return node != null;
    }

    /**
     * @param node
     * @return
     */
    protected boolean shouldAddActionBeEnabled(MutableTreeNode node) {

        return true;
    }

}
